﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Bettzueche.RLGlue.TaskSpec {
    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// See <see href="http://glue.rl-community.org/wiki/Task_Spec"/> for details.
    /// </remarks>
    class TaskSpecV30Serializer : ITaskSpecSerializer {

        #region private Fields & Constants
        
        const string _NEGATIV_INFINITY = "NEGINF";
        const string _POSITIVE_INFINITY = "POSINF";
        const string _NOT_A_NUMBER = "UNSPEC";
        /// <summary>Delimiter</summary>
        const char _DELIM = ' ';

        const string _dblRegex = @"[+-]?(\d+(\.\d+(E[+-]\d+)?)?|\.\d+|NEGINF|POSINF|UNSPEC)";
        const string _simpleDblRegex = @"[+-]?(\d+(\.\d?)?|(\.\d+))";
        const string _typesSpecRegex = @"(INTS (.*?))?(DOUBLES (.*?))?(CHARCOUNT (\d+)? )?";
        const string _intsRegex = @"\(((\d+) )?(-?\d+|NEGINF|UNSPEC) (-?\d+|POSINF|UNSPEC)\)";
        const string _doublesRegex = @"(\((\d+) )?(" + _dblRegex + ") (" + _dblRegex + @")\)";
        static readonly string _regexStr =
            @"^VERSION (\S+) PROBLEMTYPE (\S+) DISCOUNTFACTOR (" + _simpleDblRegex + ") OBSERVATIONS " +
            _typesSpecRegex + "ACTIONS " + _typesSpecRegex + @"REWARDS \((" + _dblRegex + ") (" + _dblRegex + @")\) " +
            @"EXTRA( (.+))?$";
        
        // Regex GroupNumbers as used in the Methods
        const int _GRP_VERSION = 1;
        const int _GRP_PROBLEM = 2;
        const int _GRP_DISCOUNT = 3;
        const int _GRP_OBS_INTS = 8;
        const int _GRP_OBS_DBLS = 10;
        const int _GRP_OBS_CHAR = 12;
        const int _GRP_ACT_INTS = 14;
        const int _GRP_ACT_DBLS = 16;
        const int _GRP_ACT_CHAR = 18;
        const int _GRP_REWMIN = 19;
        const int _GRP_REWMAX = 23;
        const int _GRP_EXTRA = 28;

        const int _GRP_INT_CARD = 2;
        const int _GRP_INT_MIN = 3;
        const int _GRP_INT_MAX = 4;
        
        const int _GRP_DBL_CARD = 2;
        const int _GRP_DBL_MIN = 3;
        const int _GRP_DBL_MAX = 7;

        #endregion

        #region ITaskSpecSerializer Members


        /*
         * VERSION <version-name> PROBLEMTYPE <problem-type> DISCOUNTFACTOR <discount-factor> 
         * OBSERVATIONS INTS ([times-to-repeat-this-tuple=1] <min-value> <max-value>)* 
         * DOUBLES ([times-to-repeat-this-tuple=1] <min-value> <max-value>)* 
         * CHARCOUNT <char-count> 
         * ACTIONS INTS ([times-to-repeat-this-tuple=1] <min-value> <max-value>)* 
         * DOUBLES ([times-to-repeat-this-tuple=1] <min-value> <max-value>)* 
         * CHARCOUNT <char-count> REWARDS (<min-value> <max-value>) 
         * EXTRA [extra text of your choice goes here]";
         */

        /// <summary>
        /// Serializes the specified task spec object.
        /// </summary>
        /// <param name="taskSpecObject">The task spec object.</param>
        /// <returns>
        /// The Task Spec String
        /// </returns>
        /// <exception cref="RLGlueException">if taskSpecObject is not a <see cref="TaskSpecV30"/></exception>
        public string Serialize(ITaskSpec taskSpecObject) {
            if (!(taskSpecObject is TaskSpecV30))
                throw new RLGlueException("This "+this.GetType().Name+" cannot serialize an "+taskSpecObject.GetType()+".");

            TaskSpecV30 ts = taskSpecObject as TaskSpecV30;
            StringBuilder sb = new StringBuilder();
            sb.Append("VERSION").Append(_DELIM).Append(ts.Version);
            sb.Append(_DELIM).Append("PROBLEMTYPE").Append(_DELIM).Append(ts.Propblemtype);
            sb.Append(_DELIM).Append("DISCOUNTFACTOR").Append(_DELIM).Append(ts.Discountfactor);
            sb.Append(_DELIM).Append("OBSERVATIONS").Append(_DELIM);
            sb.Append(serializeTypes(ts.Observations));
            sb.Append(_DELIM).Append("ACTIONS").Append(_DELIM);
            sb.Append(serializeTypes(ts.Actions));
            sb.Append(_DELIM).Append("REWARDS (").Append(ts.Reward.Min).Append(_DELIM).Append(ts.Reward.Max).Append(')');            
            sb.Append(_DELIM).Append("EXTRA");
            if (!String.IsNullOrEmpty(ts.Extra))
                sb.Append(_DELIM).Append(ts.Extra);

            return sb.ToString();
        }

        /// <summary>
        /// Deserializes the specified task spec string.
        /// </summary>
        /// <param name="taskSpecString">The task spec string.</param>
        /// <returns>A deseirialized instance of TaskSpecV30 or null, if doesn't conform</returns>
        public ITaskSpec Deserialize(string taskSpecString) {
            Regex regex = new Regex(_regexStr);
            Match match = regex.Match(taskSpecString);
            if (!match.Success)
                return null;

            TaskSpecV30 result = new TaskSpecV30();
            var groups = match.Groups;
            if (!groups[_GRP_VERSION].Value.StartsWith("RL-Glue-3")) {
                //Trace.TraceWarning(this.GetType() + " can not deserialize the version " + groups[_GRP_VERSION].Value);
                return null;
            }
            result.Propblemtype = groups[_GRP_PROBLEM].Value;
            try {
                //Discount:
                result.Discountfactor = Double.Parse(groups[_GRP_DISCOUNT].Value);

                //Observations:
                RLTypesSpec typesSpec = new RLTypesSpec();
                string obsInts = groups[_GRP_OBS_INTS].Value;
                string obsDbls = groups[_GRP_OBS_DBLS].Value;
                string obsCCnt = groups[_GRP_OBS_CHAR].Value;
                parseInts(typesSpec, obsInts);
                parseDbls(typesSpec, obsDbls);
                typesSpec.CharCount = Int32.Parse(obsCCnt);
                result.Observations = typesSpec;

                //Actions:
                typesSpec = new RLTypesSpec();
                string actInts = groups[_GRP_ACT_INTS].Value;
                string actDbls = groups[_GRP_ACT_DBLS].Value;
                string actCCnt = groups[_GRP_ACT_CHAR].Value;
                parseInts(typesSpec, actInts);
                parseDbls(typesSpec, actDbls);
                typesSpec.CharCount = Int32.Parse(actCCnt);
                result.Actions = typesSpec;

                //Reward:
                String rMin = groups[_GRP_REWMIN].Value;
                String rMax = groups[_GRP_REWMAX].Value;
                result.Reward = getDblRange("1", rMin, rMax);

                //Extra:
                result.Extra = groups[_GRP_EXTRA].Value;
            }
            catch (Exception) {
                return null;
            }
            return result;
        }

        #endregion

        #region Serialize Helper

        /// <summary>
        /// Serializes the types.
        /// </summary>
        /// <param name="types">The types.</param>
        /// <returns></returns>
        private String serializeTypes(RLTypesSpec types) {
            StringBuilder sb = new StringBuilder();
            sb.Append("INTS").Append(serializeIntRanges(types.Discrete));
            sb.Append("DOUBLES").Append(serializeDblRanges(types.Continuous));
            sb.Append("CHARCOUNT").Append(_DELIM).Append(types.CharCount);
            return sb.ToString();
        }
        /// <summary>
        /// Returns "( &lt;cardinality &gt;?Min Max)*" or single space " ", if ranges is empty/null.<para>
        /// cardinality is only written if it is greater 1. The String ends always with empty space.</para>
        /// </summary>
        /// <param name="ranges"></param>
        /// <returns>"( &lt;cardinality &gt;?Min Max)*" or single space " ", if ranges is empty/null.</returns>
        private String serializeDblRanges(IEnumerable<DoubleRange> ranges) {
            if (ranges == null || ranges.Count() == 0)
                return ""+_DELIM;
            StringBuilder sb = new StringBuilder();
            foreach (var range in ranges) {
                sb.Append(_DELIM).Append('(');
                if (range.Cardinality > 0) {
                    sb.Append(range.Cardinality).Append(_DELIM);
                }
                sb.Append(getDblString(range.Min));
                sb.Append(_DELIM);
                sb.Append(getDblString(range.Max));
                sb.Append(')');
            }
            sb.Append(_DELIM);
            return sb.ToString();
        }
        /// <summary>
        /// Gets the TaskSpec V3 conform String for doubles, incl. Infinity or unspec.
        /// </summary>
        /// <param name="d">value</param>
        /// <returns>TaskSpecV3 double string</returns>
        private string getDblString(double d) {
            if (d == Double.NegativeInfinity)
                return _NEGATIV_INFINITY;
            if (d == Double.PositiveInfinity)
                return _POSITIVE_INFINITY;
            if (d == Double.NaN)
                return _NOT_A_NUMBER;
            return d.ToString();
        }
        /// <summary>
        /// Returns "(&lt;cardinality&gt;? Min Max) *" or single space " ", if ranges is empty/null.<para>
        /// cardinality is only written if it is greater 1. The String ends always with empty space.</para>
        /// </summary>
        /// <param name="ranges"></param>
        /// <returns>"(&lt;cardinality&gt;? Min Max)*" or single space " ", if ranges is empty/null.</returns>
        private String serializeIntRanges(IEnumerable<IntRange> ranges) {
            if (ranges == null || ranges.Count() == 0)
                return "" + _DELIM;
            StringBuilder sb = new StringBuilder();
            foreach (var range in ranges) {
                sb.Append(_DELIM).Append('(');
                if (range.Cardinality > 0) {
                    sb.Append(range.Cardinality).Append(_DELIM);
                }
                sb.Append(range.Min).Append(_DELIM).Append(range.Max);
                sb.Append(')');
            }
            sb.Append(_DELIM);
            return sb.ToString();
        }

        #endregion

        #region Deserialize Helpers

        /// <summary>
        /// Gets the DBL range.
        /// </summary>
        /// <param name="cardinality">The cardinality.</param>
        /// <param name="min">The lower bound value</param>
        /// <param name="max">The upper bound value.</param>
        /// <exception cref=""></exception>
        private static DoubleRange getDblRange(String cardinality, String min, String max) {
            DoubleRange range = new DoubleRange();
            int card;
            if (int.TryParse(cardinality, out card))
                range.Cardinality = card;
            else
                range.Cardinality = 1;

            if (min == "")
                range.Min = Double.NegativeInfinity;
            else if (min == _NEGATIV_INFINITY)
                range.Min = Double.NegativeInfinity;
            else if (min == _NOT_A_NUMBER)
                range.Min = Double.NegativeInfinity;
            else
                range.Min = Double.Parse(min);

            if (max == "")
                range.Max = Double.PositiveInfinity;
            else if (max == _POSITIVE_INFINITY)
                range.Max = Double.PositiveInfinity;
            else if (max == _NOT_A_NUMBER)
                range.Max = Double.PositiveInfinity;
            else
                range.Max = Double.Parse(max);
            return range;
        }

        /// <summary>
        /// Parses the dbl-ranges string into the specified <see cref="RLTypesSpec"/>.
        /// </summary>
        /// <param name="typesSpec">The types spec.</param>
        /// <param name="dbls">The double ranges string.</param>
        private void parseDbls(RLTypesSpec typesSpec, string dbls) {
            // _dblRegex = @"[+-]?(\d+(\.\d+(E[+-]\d+)?)?|\.\d+|NEGINF|POSINF|UNSPEC)"
            //_doublesRegex = @"(\((\d+) )?(" + _dblRegex + ") (" + _dblRegex + @")\)";
            // g2=dRange g3=Min g7=Max
            Regex regex = new Regex(_doublesRegex);
            Match match = regex.Match(dbls);
            while (match.Success) {
                string cardi = match.Groups[_GRP_DBL_CARD].Value;
                string min   = match.Groups[_GRP_DBL_MIN].Value;
                string max   = match.Groups[_GRP_DBL_MAX].Value;
                typesSpec.AddContinuous(getDblRange(cardi, min, max));
                match = match.NextMatch();
            }
        }

        /// <summary>
        /// Parses the int-ranges string into the specified <see cref="RLTypesSpec"/>.
        /// </summary>
        /// <param name="typesSpec">The types spec.</param>
        /// <param name="ints">The int ranges string (without "INTS")</param>
        /// <exception cref="FormatException"/>
        private void parseInts(RLTypesSpec typesSpec, string ints) {
            // @"\(((\d+) )?(-?\d+|NEGINF|UNSPEC) (-?\d+|POSINF|UNSPEC)\)"
            //g2=cardinality g3=Min g4=Max
            Regex regex = new Regex(_intsRegex);
            Match match = regex.Match(ints);
            while (match.Success) {
                IntRange iRange = new IntRange();
                if (match.Groups[_GRP_INT_CARD].Value == "")
                    iRange.Cardinality = 1;
                else
                    iRange.Cardinality = int.Parse(match.Groups[_GRP_INT_CARD].Value);

                string min = match.Groups[_GRP_INT_MIN].Value;
                if (min == _NEGATIV_INFINITY || min == _NOT_A_NUMBER)
                    iRange.Min = Int32.MinValue;
                else
                    iRange.Min = Int32.Parse(min);

                string max = match.Groups[_GRP_INT_MAX].Value;
                if (max == _POSITIVE_INFINITY || max == _NOT_A_NUMBER)
                    iRange.Max = Int32.MaxValue;
                else
                    iRange.Max = Int32.Parse(max);

                typesSpec.AddDiscrete(iRange);
                match = match.NextMatch();
            }
        }

        #endregion

        
    }
}
